package com.mikelouis.test.scala.proginscala.scalacheck

/**
 * 属性 property 是ScalaCheck中的可测试单元, 对应的类是org.scalacheck.Prop. 在ScalaCheck中有几种创建属性的方式，
 * 其中之一是使用org.scalacheck.Prop.forAll函数，如前例所示。例子中forAll直接创建了通用的量化属性，也可以通过组合其他属性创建新的属性，
 * 或者通过使用org.scalacheck.Prop 对象的特化函数来创建。
 *
 * Created by yuazhu on 4/18/2015.
 */
object Properties {
  /** Universally quantified properties */
  // org.scalacheck.Prop.forAll创建通用的量化属性。forAll接受一个函数作为参数,并且使用它创建了一个属性，
  // 这个属性可以通过check方法来测试。这个作为参数的函数应该返回Boolean或者其他属性, 并且可以接受任何类型的参数，
  // 只要针对这些类型存在(隐式转换可以转换出来)隐式Arbitrary实例。ScalaCheck缺省包含了通用类型如Int, String,
  // List等的隐式Arbitrary实例，也可以定义自己的Arbitrary实例

  import org.scalacheck.Prop.forAll

  val propReverseList = forAll { (l: List[String]) => l.reverse.reverse == l}
  val propConcatString = forAll { (s1: String, s2: String) => (s1 + s2).endsWith(s2)}

  // 可以给forAll提供一个特定的数据生成器。请看下例:
  val smallInteger = org.scalacheck.Gen.choose(0, 100)
  val propSmallInteger = forAll(smallInteger) { (n: Int) => n >= 0 && n <= 100}

  /** Conditional Properties */
  // 某些情况下, 属性声明采用蕴含的形式。在ScalaCheck中可以使用==>蕴含操作符

  import org.scalacheck.Prop.{forAll, BooleanOperators}

  // Now ScalaCheck will only care for the cases when n is not negative. We also filter out large numbers, since we don't want to generate huge lists
  val propMakeList = forAll { (n: Int) => (n >= 0 && n < 10000) ==> (List.make(n, "").length == n)}

  // 如果蕴含操作符的给定条件很难或者不可能实现，ScalaCheck可能无法找到足够多的可接受测试用例，因此无法声明属性能够hold住
  val propTrivial = forAll((n: Int) => (n == 0) ==> (n == 0))

  /** Combining Properties */
  // 第三种创建属性的方式是通过组合现有属性来构建新的属性
  // p3在且仅在p1和p2为真时为真, p4在p1为真或者p2为真时为真, p5在 p2为真时p1为真且反之p1为真时p2为真都成立时为真
  // val p1 = forAll(...)
  // val p2 = forAll(...)
  // val p3 = p1 && p2
  // val p4 = p1 || p2
  // val p5 = p1 == p2
  // val p6 = all(p1, p2) // 同p1 && p2
  // val p7 = atLeastOne(p1, p2) // 同 p1 || p2

  /** Grouping properties */
  // 常常你需要声明相关的属性,比如可能为某类的所有方法声明属性。ScalaCheck为这种需求提供了简单的方法，通过使用Properties特质

  import org.scalacheck.{Properties, Prop}

  object StringSpecification extends Properties("String") {

    import Prop.forAll

    property("startsWith") = forAll { (a: String, b: String) =>
      (a + b).startsWith(a)
    }

    property("endsWith") = forAll { (a: String, b: String) =>
      (a + b).endsWith(b)
    }

    property("substring") = forAll { (a: String, b: String) =>
      (a + b).substring(a.length) == b
    }

    property("substring") = forAll { (a: String, b: String, c: String) =>
      (a + b + c).substring(a.length, a.length + b.length) == b
    }
  }

  // 可以使用Properties.include方法来分组几个不同的属性集合为一个集合。例如可以为应用创建一个属性集合, 这个集合包含应用中所有的各类的属性集合
  // object MyAppSpecification extends Properties("MyApp") {
  //   include(StringSpecification)
  //   include(...)
  //   include(...)
  // }

  /** Labeling Properties */
  // 有时侯确定声明属性失败的原因很困难，特别是当属性很复杂，具有很多检查条件时。这些情况下，可以字符串标记属性的不同部分，
  // 然后ScalaCheck能够精确的告诉我们失败条件所在部分

  import org.scalacheck.Prop.{forAll, BooleanOperators}

  val complexProp = forAll { (m: Int, n: Int) =>
    val res = myMagicFunction(n, m)
    (res >= m) :| "result > #1" &&
      (res >= n) :| "result > #2" &&
      (res < m + n) :| "result not sum"
  }

  // 也可以像下面例子这样在属性条件之前写标签

  import org.scalacheck.Prop.{forAll, BooleanOperators}

  val complexProp2 = forAll { (m: Int, n: Int) =>
    val res = myMagicFunction(n, m)
    ("result < #1" |: res <= m) &&
      ("result < #2" |: res <= n) &&
      ("result not sum" |: res < m + n)
  }

  def myMagicFunction(n: Int, m: Int): Int = n + m

  // 标签操作符也能够被用于查看属性中使用的中间值, 这有助于厘清导致属性测试失败的原因。
  // 诚然ScalaCheck会展示随机生成的属性参数值(ARG_0、 ARG_1 等), 不过有时需要快速确定中间运算的值

  import org.scalacheck.Prop.{forAll, BooleanOperators, all}

  // 此处有四个不同的条件，每个都有自己的标记。这里没有使用条件的&&运算符， 而是使用Prop.all方法等效的将条件组合在一起。中间使用的蕴含操作符==>可以避免除0错误。 对组合在一起的属性添加了第五个标签，这个标签来记录乘法运算的结果
  val propMul = forAll { (n: Int, m: Int) =>
    val res = n * m
    ("evidence = " + res) |: all(
      "div1" |: m != 0 ==> (res / m == n),
      "div2" |: n != 0 ==> (res / n == m),
      "lt1" |: res > m,
      "lt2" |: res > n
    )
  }

  // 可以看出, 可以为你的属性添加你希望的尽可能多的标签, 如果属性测试失败, ScalaCheck会显示这些标签


  def main(args: Array[String]) {
    propReverseList.check
    propConcatString.check
    propSmallInteger.check
    propMakeList.check
    propTrivial.check

    // Properties类包含一个main方法，可以简单的执行这些属性测试
    StringSpecification.main(args) // or StringSpecification.check

    complexProp.check
    complexProp2.check
    propMul.check
  }
}
